在軟體開發當中幾乎都會聽過的 SOLID 原則,其源自於 Robert C. Martin,原文如下
"A class should have only one reason to change"
翻譯成中文為「一個類別應該只有一個改變的理由」,今天我們就來提及關於單一職責原則,在程式當中的意思簡單的說法就是:每個函式、類別或模組都應該只負責一件事,並且只有一個改變的理由。
換句話說,當我們需要修改程式碼時,每次的修改應該只影響單一功能。這個原則能讓我們的程式碼更容易理解、測試和維護。
我們用生活中的例子來舉例,例如主廚專注煮菜、掌控味道與擺盤,服務生則負責端菜、與客人互動並結帳;如果同一個人既要煮又要跑桌,可能會忙成一團、出菜變慢、顧客等待時間拉長,也很難針對某一流程做優化。
對應到程式設計,就是不要把使用者介面、金額計算、輸入驗證與 API 呼叫全塞在同一個元件或函式裡——應該把「計算金額」「驗證輸入」「呼叫 API」「顯示 UI」各自拆開負責,讓每個部分都能專注且容易維護。
下面用同一個業務邏輯示範兩個版本
這個範例違反了單一職責原則,因為它將 UI 呈現、業務邏輯、資料驗證、資料存取以及資料格式化等多種職責全部混在同一個元件裡。當需求變動時,例如稅率調整、驗證規則更新或 API 格式更改,都會迫使這個我們需要找尋需要修改的位置,當業務邏輯上升,以許未來資料驗證邏輯更多、需要格式化以及存取的部分更多,則要找尋程式碼的地方就像大海撈針一樣,導致維護成本提高、測試困難度增加,也讓程式碼的可讀性與可重用性大幅下降。
import React, { useState } from "react";
export const OrderCheckoutBad: React.FC = () => {
const [quantity, setQuantity] = useState(1);
const [price, setPrice] = useState(100);
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (quantity <= 0 || price <= 0) { alert("數量/單價需大於 0"); return; }
if (!/^\S+@\S+$/.test(email)) { alert("Email 格式錯誤"); return; }
const subtotal = quantity * price;
const tax = Math.round(subtotal * 0.05);
const total = subtotal + tax;
const currency = new Intl.NumberFormat("zh-TW",{style:"currency",currency:"TWD"}).format(total);
try {
setLoading(true);
const response = await fetch("/api/checkout",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({ qty: quantity, price, email, total })
});
if (!response.ok) throw new Error("network");
alert(`下單成功,金額:${currency}`);
} catch {
alert("下單失敗,請稍後再試");
} finally {
setLoading(false);
}
};
return (
<div>
<h3>Checkout (Bad)</h3>
<input
type="number"
value={quantity}
onChange={(event) => setQuantity(+event.target.value)}
/>{" "}
<input
type="number"
value={price}
onChange={(event) => setPrice(+event.target.value)}
/>{" "}
<input
placeholder="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>{" "}
<button disabled={loading} onClick={handleSubmit}>
{loading ? "處理中…" : "送出訂單"}
</button>
<p>預估總計:{quantity * price + Math.round(quantity * price * 0.05)}</p>
</div>
);
};
下面這個範例就符合 SRP 原則,因為它將不同的職責清楚拆分:金額計算由 calculateTotal 負責,驗證規則集中在 validateOrder,金額格式化交給 formatTWD,與 API 溝通的邏輯則獨立在 checkoutApi,而 React 元件本身只專注於狀態管理與 UI 呈現。這種分工方式讓每個模組都有單一變動原因,例如修改稅率只需調整 TAX_RATE 與計算函式、變更驗證邏輯只需改 validateOrder,不會影響其他部分,讓程式碼更易讀、易測試。
type Order = { qty: number; price: number; email: string; };
const TAX_RATE = 0.05;
const calculateTotal = ({ qty, price }: Order) => {
const subtotal = qty * price;
const tax = Math.round(subtotal * TAX_RATE);
return { subtotal, tax, total: subtotal + tax };
};
const validateOrder = ({ qty, price, email }: Order) =>
qty <= 0 ? "數量需大於 0" :
price <= 0 ? "單價需大於 0" :
!/^\S+@\S+$/.test(email) ? "Email 格式錯誤" :
null;
const formatTWD = (amount: number) =>
new Intl.NumberFormat("zh-TW", {style:"currency", currency:"TWD"}).format(amount);
const checkoutApi = async (orderData: Order & { total: number }) =>
Promise.resolve({ ok: true });
export const OrderCheckoutGood: React.FC = () => {
const [form, setForm] = useState<Order>({ qty: 1, price: 100, email: "" });
const [loading, setLoading] = useState(false);
const totals = calculateTotal(form);
const handleSubmit = async () => {
const error = validateOrder(form);
if (error) {
alert(error);
return;
}
setLoading(true);
const response = await checkoutApi({ ...form, total: totals.total });
setLoading(false);
alert(response.ok ? `下單成功,金額:${formatTWD(totals.total)}` : "下單失敗");
};
return (
<div>
<h3>Checkout (Good)</h3>
<input
type="number"
value={form.qty}
onChange={(event) => setForm({ ...form, qty: +event.target.value })}
/>{" "}
<input
type="number"
value={form.price}
onChange={(event) => setForm({ ...form, price: +event.target.value })}
/>{" "}
<input
placeholder="email"
value={form.email}
onChange={(event) => setForm({ ...form, email: event.target.value })}
/>{" "}
<button disabled={loading} onClick={handleSubmit}>
{loading ? "處理中…" : "送出訂單"}
</button>
<p>預估總計:{formatTWD(totals.total)}</p>
</div>
);
};
綜合以上所述,我們成功透過單一功能原則讓程式碼變得更加模組化和可維護。記住,好的程式碼應該讓每個函式都有明確的單一目的。當我們在寫程式時,可以時常問自己:「這個函式是否只做一件事?」